Skip to main content

DivsGlobalVars

Global State vs. Dependency Injection in Go: Trade-offs and Best Practices

Introduction**

When structuring backend applications, one of the key decisions is whether to use global state or dependency injection (DI) for managing shared resources like database connections.

We are using a go backend as our example here, but of course this applies to any application.


Understanding Global State in Go**

What It Is

  • Defining variables at the package level:

    package db

    import (
    "database/sql"
    _ "github.com/lib/pq"
    )

    var DB *sql.DB
    var Queries *Queries
  • These variables are initialized once and shared across the application.

Pros of Using Global State

Simplifies function signatures – No need to pass dependencies everywhere.
Cleaner code in small projects – Reduces boilerplate.
Good for truly global, single-instance resources like a database connection.

Cons of Using Global State

Harder to test – You can't easily swap dependencies in tests.

  • That said, I personally use
    Implicit dependencies – Functions rely on hidden state instead of explicit parameters.
    Risk of unintended side effects – If reinitialized, everything depending on it changes.

Understanding Dependency Injection in Go**

What It Is

  • Instead of using package-level variables, dependencies are explicitly created and passed around:
    package db

    func NewDB() (*sql.DB, *Queries, error) {
    db, err := sql.Open("postgres", "dsn_here")
    if err != nil {
    return nil, nil, err
    }
    queries := New(db)
    return db, queries, nil
    }
    package main

    import "myapp/db"

    func main() {
    dbConn, queries, err := db.NewDB()
    if err != nil {
    log.Fatal(err)
    }
    startServer(dbConn, queries)
    }
    func startServer(db *sql.DB, queries *Queries) {
    handler := NewHandler(db, queries)
    http.ListenAndServe(":8080", handler)
    }

Pros of Dependency Injection

Explicit dependencies – Clearer about what a function needs.
Easier to test – Can inject mock implementations in unit tests. *See Note ✅ More flexible – Supports multiple instances (e.g., different DB connections).

Cons of Dependency Injection

Function signatures become long – Need to pass dependencies explicitly.
More boilerplate – Requires more struct initialization and passing.


When to Use Each Approach**

ScenarioGlobal StateDependency Injection
Small apps, scripts🚫
Large applications🚫
Testing with mock databases🚫
Keeping function signatures clean🚫
Managing multiple DB instances🚫

Conclusion**

  • There’s no universal “right” choice—it depends on your project.
  • Use global state for simplicity when it makes sense.
  • Use dependency injection when you need testability and flexibility.
  • Hybrid approaches can work, but be careful with global modifications.
  • In your case, switching to global state helped clean up function signatures while keeping things manageable.

** A note on Testability * - I personally use E2E API call tests, since E2E tests can validate the entire flow. Therefore for typical CRUD backends i dont have unit tests for handlers. This makes dependency injection less critical for testability for this specific use case, but of course there may be other situations where I choose DI specifically for testability reasons.


This captures the trade-offs you’ve been working through in real-time. Want to add anything else before we refine it into a full article?